/*
 * Copyright (c) 2012 by Paweł Lebioda <pawel.lebioda89@gmail.com>
 *
 * This file is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/ioctl.h>
#include <linux/sched.h>
#include <linux/capability.h>

#include <asm/uaccess.h>

#include "random.h"

#ifdef DEBUG
#define DEBUG_NAME	"[DEBUG] random"
#define DEBUG_PRIOR	KERN_INFO
#endif 
#include "debug.h"


/* macros */
#define MODULE_NAME		"random"
#define RANDOM_q		127773
#define RANDOM_r		2836
#define RANDOM_m		2147483647
#define RANDOM_a		16807
#define RANDOM_MAX_INT		RANDOM_m
#define RANDOM_MAX_SHORT	((1<<(8*sizeof(short)))-1)
#define RANDOM_MAX_CHAR		((1<<(8*sizeof(char)))-1)
#define RANDOM_MAJOR		0
#define RANDOM_MINOR		0
#define RANDOM_SIZE		4
#define RANDOM_CORE		1
#define RANDOM_MIN		0
#define RANDOM_MAX		RANDOM_MAX_INT
#define WARNING(fmt,args...)	printk(KERN_WARNING "[warning] %s: " fmt,MODULE_NAME,## args)
#define ERROR(fmt,args...)	printk(KERN_WARNING "[error] %s: %s: " fmt,MODULE_NAME,__func__,## args)
#define CHECK_CONTEXT(context,err)	if(!(context)) {			\
						ERROR("context==NULL");		\
						return (err);			\
					}					\

#define RANDOM_CAPABILITY	CAP_SYS_ADMIN
/* declarations */
struct random_context;
struct random_gen;

typedef int random_mode_t;

typedef int random_type_t;
#define RANDOM_TYPE_INT		0

/* structs */
struct random_context 
{
	random_mode_t	mode;
	struct random_gen * gen;	
};

struct random_gen
{
	struct random_type type; 
	int core;
};

union random_int
{
	int 	int32;
	short 	int16[2];
	char 	int8[4];
};

/* internal functions */
static int  __init random_init(void);
static void __exit random_exit(void);
static int  random_check_params(void);
static void random_cleanup(void);
static int random_gen_uniform(int * core);
static int random_gen_int(struct random_gen * gen);
static struct random_context * random_context_alloc(void);
static struct random_gen * random_gen_alloc(void);
static ssize_t random_gen_read(struct random_gen * gen,char __user * buff,size_t count,loff_t * f_pos);
static int random_gen_check_type(struct random_type * type);
static int random_gen_set_type(struct random_gen * gen,struct random_type * type);
static long random_context_ioctl(struct random_context * gen,unsigned int cmd,unsigned long arg);
static int random_context_set_mode(struct random_context * context,int mode);

/* file_operations functions*/
int random_open(struct inode * inodep,struct file * filp);
int random_release(struct inode * inodep,struct file * filp);
ssize_t random_read(struct file * filp,char __user * buff,size_t count,loff_t * f_pos);
ssize_t random_write(struct file * filp,const char __user * buff,size_t count,loff_t * f_pos);
long random_ioctl(struct file * filp,unsigned int cmd,unsigned long arg);

/* global variables */
static int random_major = RANDOM_MAJOR;
static int random_minor = RANDOM_MINOR;
static dev_t random_devno;
struct semaphore random_global_sem;
struct cdev random_cdev;
struct file_operations random_fops = {
	.owner 		= THIS_MODULE,
	.open 		= random_open,
	.release	= random_release, 
	.read		= random_read,
	.write		= random_write,
	.unlocked_ioctl = random_ioctl
};

struct random_gen random_global;


/* Module's info */
MODULE_AUTHOR("pako <pawel.lebioda89@gmail.com");
MODULE_LICENSE("GPL v2");
MODULE_VERSION("1.0");
MODULE_DESCRIPTION("Pseudo-random numbers generator");

/* parameters */
module_param(random_major,int,S_IRUGO);
module_param(random_minor,int,S_IRUGO);
MODULE_PARM_DESC(random_major,"device's major number");
MODULE_PARM_DESC(random_minor,"device's minor number");


static int random_gen_uniform(int * core)
{
	int x = *core;
	int h = x/RANDOM_q;
	x = RANDOM_a*(x-RANDOM_q*h) - RANDOM_r*h;
	if(x<0)
		x += RANDOM_m;
	*core = x;
	return x; 
}

static int random_gen_int(struct random_gen * gen)
{
	int x;
	x = random_gen_uniform(&gen->core);
	x = (x%(gen->type.interval.max-gen->type.interval.min+1)) + gen->type.interval.min;
	return x;
}

static struct random_context * random_context_alloc(void)
{
	struct random_context * context;
	DEBUG_PRINT();
	context = kmalloc(sizeof(struct random_context),GFP_KERNEL);
	CHECK_CONTEXT(context,NULL);
	memset(context,0,sizeof(struct random_context));
	return context;
}

static struct random_gen * random_gen_alloc(void)
{
	struct random_gen * gen;
	gen = kmalloc(sizeof(struct random_gen),GFP_KERNEL);
	if(!gen) {
		ERROR("gen==NULL");
		return NULL;
	}
	memset(gen,0,sizeof(struct random_gen));
	return gen;
}

void random_gen_init(struct random_gen * gen)
{
	gen->type.size = RANDOM_SIZE;
	gen->core = RANDOM_CORE;
	gen->type.interval.min = RANDOM_MIN;
	gen->type.interval.max = RANDOM_MAX;
}

static int random_check_params(void)
{
	if(random_major < 0) {
		WARNING("invalid value of param: major -- '%d' (major number must be zero or positive)",random_major);
		return -EINVAL;	
	}
	if(random_minor < 0 ) {
		WARNING("invalid value of param: minor -- '%d' (minor number must be zero or positive)",random_minor);
		return -EINVAL;
	}
	return 0;
}

static int __init random_init(void)
{			
	int ret;	
	DEBUG_PRINT();	
	random_gen_init(&random_global);
	/* check module parameters first */
	ret = random_check_params();
	if(ret) return ret;
	if(random_major) {	/* if nonzero static registration */
		random_devno = MKDEV(random_major,random_minor);
		ret = register_chrdev_region(random_devno,1,MODULE_NAME);
	} else {		/* if zero dynamic registration   */
		ret = alloc_chrdev_region(&random_devno,random_minor,1,MODULE_NAME);
		random_major = MAJOR(random_devno);
	}
	if(ret<0) {
		WARNING("can't get (major,minor): (%d,%d)",random_major,random_minor);
		return ret;
	}
	DEBUG_PRINT("registered (major,minor): (%d,%d)",random_major,random_minor);
	cdev_init(&random_cdev,&random_fops);
	random_cdev.owner = THIS_MODULE;
	random_cdev.ops = &random_fops;
	sema_init(&random_global_sem,1);
	ret = cdev_add(&random_cdev,random_devno,1);
	if(ret) goto fail;
	return 0;
fail:
	random_cleanup();
	return ret;	
}			
module_init(random_init);

static void __exit random_exit(void)
{
	random_cleanup();
	DEBUG_PRINT();
}
module_exit(random_exit);

static void random_cleanup(void)
{
	cdev_del(&random_cdev);
	unregister_chrdev_region(random_devno,1);
}

int random_open(struct inode * inodep,struct file * filp)
{
	struct random_context * context;
	DEBUG_PRINT();
	context = random_context_alloc();
	CHECK_CONTEXT(context,-ENOMEM);
	context->mode = RANDOM_MODE_GLOBAL;
	context->gen = &random_global;
	filp->private_data = context;
	return 0;
}

int random_release(struct inode * inodep,struct file * filp)
{
	struct random_context * context;
	DEBUG_PRINT();
	context = (struct random_context*)filp->private_data;
	CHECK_CONTEXT(context,-EFAULT);
	if(context->mode == RANDOM_MODE_LOCAL) {
		kfree(context->gen);
		DEBUG_PRINT("kfree local");
	}
	kfree(context);
	DEBUG_PRINT("kfree context");
	filp->private_data = NULL;
	return 0;
}

				

ssize_t random_read(struct file * filp,char __user * buff,size_t count,loff_t * f_pos)
{
	struct random_context * context;
	int ret=0;
	context = (struct random_context*)filp->private_data;
	CHECK_CONTEXT(context,-EFAULT);
	if(!access_ok(VERIFY_READ,buff,count))
		return -EFAULT;
	if(context->mode == RANDOM_MODE_LOCAL) {
		return random_gen_read(context->gen,buff,count,f_pos);	
	}
	if(down_interruptible(&random_global_sem))
		return -ERESTARTSYS;
	ret = random_gen_read(&random_global,buff,count,f_pos);	
	up(&random_global_sem);
	return ret; 
	
}

ssize_t random_gen_read(struct random_gen * gen,char __user * buff,size_t count,loff_t * f_pos)
{
	/*
	* NOTE:	assumed that user's space buffer is checked by access_ok in random_read func.
	*/
	int ret,i,x,count_int,count_rest,size;
	int (*generate)(struct random_gen *);
	generate = random_gen_int;
	size = gen->type.size;
	count_int = count/size;
	count_rest = count%size;
	for(i=0;i<count_int;i++) {
		x = generate(gen);
		if(__copy_to_user(buff,&x,size)) {
			ret = i*size;
			*f_pos += ret;
			return ret; 
		}
		buff+= size;
	}
	if(__copy_to_user(buff,&x,count_rest)) {
		ret = count_int*size;
		*f_pos += ret;
		return ret;
	}	
	*f_pos += count;
	return count;
}

ssize_t random_write(struct file * filp,const char __user * buff,size_t count,loff_t * f_pos)
{
	struct random_context * context;
	DEBUG_PRINT();	
	context = filp->private_data;
	CHECK_CONTEXT(context,-EFAULT);
	
	*f_pos += count;
	return count;
}

long random_ioctl(struct file * filp,unsigned int cmd,unsigned long arg)
{
	struct random_context * context;
	int ret=0;
	DEBUG_PRINT();
	context = filp->private_data;
	CHECK_CONTEXT(context,-EFAULT);
	if(_IOC_TYPE(cmd) != RANDOM_IOC_MAGIC) return -ENOTTY;
	if(_IOC_NR(cmd) > RANDOM_IOC_MAX) return -ENOTTY;
	if(_IOC_DIR(cmd) & _IOC_READ)
		ret = !access_ok(VERIFY_READ,(void __user*)arg,_IOC_SIZE(cmd));
	else if (_IOC_DIR(cmd) & _IOC_WRITE)
		ret = !access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
	if(ret) return -EFAULT;
	
	if(context->mode == RANDOM_MODE_GLOBAL) {
		if(_IOC_DIR(cmd) & _IOC_WRITE) {
			/* check capability for specified commands */
			switch(cmd) {
				case RANDOM_IOC_RESET:
				case RANDOM_IOC_SET_SIZE:
				case RANDOM_IOC_SET_MIN:
				case RANDOM_IOC_SET_MAX:
				case RANDOM_IOC_SET_CORE:
					if(!capable(RANDOM_CAPABILITY))
						return -EPERM;
					break;
			}
			/* global generator and write command need semaphore */
			if(down_interruptible(&random_global_sem))
				return -ERESTARTSYS;
			DEBUG_PRINT("random_global_sem: down_interruptible");
			ret = random_context_ioctl(context,cmd,arg);
			up(&random_global_sem);
			DEBUG_PRINT("random_global_sem: up");
			return ret;
		}
	}
	/* 
	* even if gen points to random_global, if cmd type is _IOC_READ there is no 
	* need to use semaphore
	*/
	return random_context_ioctl(context,cmd,arg);
}

static long random_context_ioctl(struct random_context * context,unsigned int cmd,unsigned long arg)
{
	int ret=0,tmp;
	struct random_gen * gen = context->gen;
	struct random_type type = gen->type;
	DEBUG_PRINT();
	/*
	* NOTE: assumed that arg pointer given from user is checked by access_ok in random_gen_ioctl
	*	and capability is checked
	*/
	switch(cmd) {
		case RANDOM_IOC_RESET:
			DEBUG_PRINT("RANDOM_IOC_RESET");
			random_gen_init(gen);
			return 0;
		case RANDOM_IOC_SET_SIZE:
			DEBUG_PRINT("RANDOM_IOC_SET_SIZE");
			ret = __get_user(type.size,(int __user*)arg);
			if(ret) return ret;
			ret = random_gen_set_type(gen,&type);
			break;
		case RANDOM_IOC_GET_SIZE:
			DEBUG_PRINT("RANDOM_IOC_GET_SIZE");
			ret = __put_user(type.size,(int __user*)arg);
			break;
		case RANDOM_IOC_SET_MIN:
			DEBUG_PRINT("RANDOM_IOC_SET_MIN");
			ret = __get_user(type.interval.min,(int __user*)arg);
			if(ret) return ret;
			ret = random_gen_set_type(gen,&type);
			break;
		case RANDOM_IOC_GET_MIN:
			DEBUG_PRINT("RANDOM_IOC_GET_MIN");
			ret = __put_user(type.interval.min,(int __user*)arg);
			break;
		case RANDOM_IOC_SET_MAX:
			DEBUG_PRINT("RANDOM_IOC_SET_MAX");
			ret = __get_user(type.interval.max,(int __user*)arg);
			if(ret) return ret;
			ret = random_gen_set_type(gen,&type);
			break;
		case RANDOM_IOC_GET_MAX:
			DEBUG_PRINT("RANDOM_IOC_GET_MAX");
			ret = __put_user(type.interval.max,(int __user*)arg);
			break;
		case RANDOM_IOC_SET_INTERVAL:
			DEBUG_PRINT("RANDOM_IOC_SET_INTERVAL");
			ret = __copy_from_user(&type.interval,(struct random_interval __user*)arg,sizeof(struct random_interval));
			if(ret) return ret;
			ret = random_gen_set_type(gen,&type);
			break;
		case RANDOM_IOC_GET_INTERVAL:
			DEBUG_PRINT("RANDOM_IOC_GET_INTERVAL");
			ret = __copy_to_user((struct random_interval __user*)arg,&type.interval,sizeof(struct random_interval));
			break;
		case RANDOM_IOC_SET_TYPE:
			DEBUG_PRINT("RANDOM_IOC_SET_TYPE");
			ret = __copy_from_user(&type,(struct random_type __user*)arg,sizeof(struct random_type));
			if(ret) return ret;
			ret = random_gen_set_type(gen,&type);
			break;
		case RANDOM_IOC_GET_TYPE:
			DEBUG_PRINT("RANDOM_IOC_GET_TYPE");
			ret = __copy_to_user((struct random_type __user*)arg,&type,sizeof(struct random_type));
			break;
		case RANDOM_IOC_SET_CORE:
			DEBUG_PRINT("RANDOM_IOC_SET_CORE");
			ret = __get_user(tmp,(int __user*)arg);
			if(ret) return ret;
			gen->core = tmp;
			break;
		case RANDOM_IOC_GET_CORE:
			DEBUG_PRINT("RANDOM_IOC_GET_CORE");
			ret = __put_user(gen->core,(int __user*)arg);
			break;
		case RANDOM_IOC_SET_MODE:
			DEBUG_PRINT("RANDOM_IOC_SET_MODE");
			ret = __get_user(tmp,(int __user*)arg);
			if(ret) return ret;
			ret = random_context_set_mode(context,tmp);
			break;
		case RANDOM_IOC_GET_MODE:
			DEBUG_PRINT("RANDOM_IOC_GET_MODE");
			ret = __put_user(context->mode,(int __user*)arg);
			break;
		case RANDOM_IOC_MODE_LOCAL:
			DEBUG_PRINT("RANDOM_IOC_MODE_LOCAL");
			ret = random_context_set_mode(context,RANDOM_MODE_LOCAL);
			break;
		case RANDOM_IOC_MODE_GLOBAL:
			DEBUG_PRINT("RANDOM_IOC_MODE_GLOBAL");
			ret = random_context_set_mode(context,RANDOM_MODE_GLOBAL);
			break;
		default:
			ret = -ENOTTY;
			break;
	}
	return ret;
}

static int random_gen_check_type(struct random_type * type)
{
	int diff = type->interval.max - type->interval.min;
	if(type->interval.min>type->interval.max) return -EINVAL;
	switch(type->size) {
		case 4:
			if(diff > RANDOM_MAX_INT)
				return -EINVAL;
			break;	
		case 2:
			if(diff > RANDOM_MAX_SHORT)
				return -EINVAL;
			break;
		case 1:
			if(diff > RANDOM_MAX_CHAR)
				return -EINVAL;
			break;
		default:
			return -EINVAL;
	}
	return 0;

}

static int random_gen_set_type(struct random_gen * gen,struct random_type * type)
{
	int ret = random_gen_check_type(type);
	if(ret) return ret;
	memcpy(&gen->type,type,sizeof(struct random_type));
	return 0;
}

static int random_context_set_mode(struct random_context * context,int mode)
{
	DEBUG_PRINT();
	/* check valid mode to change*/
	if(mode != RANDOM_MODE_LOCAL &&
	   mode != RANDOM_MODE_GLOBAL) {
		return -EINVAL;
	}	
	if(context->mode == RANDOM_MODE_LOCAL) {
	/* current mode is global*/
		DEBUG_PRINT("current=local");
		if(mode == RANDOM_MODE_GLOBAL) {
		/*
		* user changes mode to global so we have to free 
		* local generator and save global generator to context
		*/
			DEBUG_PRINT("kfree local");
			kfree(context->gen);
			context->gen = &random_global;
		}
	} else {
	/* current mode is global */
		DEBUG_PRINT("current=global");
		if(mode == RANDOM_MODE_LOCAL) {
		/*
		* user changes mode to local so we have to allocate 
		* and initialize local generator
		*/	
			context->gen = random_gen_alloc();	
			if(!context->gen) {
			/*
			* something's wrong so we do not change mode and 
			* save global generator pointer to context 
			*/
				ERROR("!context->gen");
				context->gen = &random_global;
				return -ENOMEM;
			}
			DEBUG_PRINT("local gen alloc");
			/* initialization of local generator */
			random_gen_init(context->gen);
			DEBUG_PRINT("local gen init");
			/* 
			* copy global generator's core to increase ranomness 
			* user can set core or reset t default using another ioctl cmd
			*/
			context->gen->core = random_global.core;
		}
	}
	context->mode = mode;
	return 0;
}

